-
Notifications
You must be signed in to change notification settings - Fork 0
Feat: [FN-141][FN-142][FN-143] 플래시 카드셋 상세/목록 조회 및 수정 기능 #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Caution Review failedThe pull request is closed. Walkthrough카드셋 API를 목록(검색)/상세/수정 중심으로 재구성하고, 그룹 경로로 생성/조회/수정 컨트롤러를 분리했다. 페이징 공통 모델(PagingRequest/PagingResponse/CursorPagingRequest)과 커서/페이지 응답형을 통합·리네임했으며, 권한·정책 서비스, 그룹 초대 이벤트·이메일 발송 및 관련 저장소 쿼리·변환 로직을 추가·조정했다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant C as Client
participant CC as CardSetController
participant S as CardSetService
participant R as CardSetRepository
rect rgb(245,248,255)
note over C,CC: 카드셋 목록 조회 (검색+페이징)
C->>CC: GET /v1/card-sets?keyword&category&page&size
CC->>S: getCardSets(searchReq)
S->>R: findByNameContainingAndCategory(..., PageRequest)
R-->>S: Page<CardSet>
S-->>CC: PagingResponse<CardSetSummaryResponse>
CC-->>C: 200 OK + PagingResponse
end
sequenceDiagram
autonumber
participant C as Client
participant GC as GroupCardSetController
participant PS as CardSetPolicyService
participant S as CardSetService
participant R as CardSetRepository
participant GS as GroupService
rect rgb(245,255,245)
note over C,GC: 그룹 카드셋 상세 조회
C->>GC: GET /v1/groups/{groupId}/card-sets/{cardSetId}
GC->>S: getCardSet(userId, groupId, cardSetId)
S->>PS: findByIdAndGroupIdOrThrow(groupId, cardSetId)
PS->>R: findByIdAndGroup_Id(...)
R-->>PS: CardSet | not found
PS-->>S: CardSet or throws NOT_FOUND
S->>PS: validateCardSetViewable(cardSet, userId, groupId)
PS->>GS: existsMember(groupId, userId)
GS-->>PS: boolean
PS-->>S: ok or throws FORBIDDEN
S-->>GC: CardSetDetailResponse
GC-->>C: 200 OK
end
rect rgb(255,249,240)
note over C,GC: 그룹 카드셋 수정
C->>GC: PUT /v1/groups/{groupId}/card-sets/{cardSetId}
GC->>S: updateCardSet(userId, groupId, cardSetId, req)
S->>PS: findByIdAndGroupIdOrThrow(...)
PS->>R: findByIdAndGroup_Id(...)
R-->>PS: CardSet
PS-->>S: CardSet
S->>PS: validateCardSetEditable(userId, cardSetId)
PS-->>S: ok or throws FORBIDDEN
S->>S: apply CardSet.update(payload)
S->>R: saveAndFlush(cardSet)
R-->>S: CardSet
S-->>GC: CardSetDetailResponse
GC-->>C: 200 OK
end
sequenceDiagram
autonumber
participant C as Client
participant Q as GroupInvitationQueryController
participant S as GroupInvitationService
participant R as GroupInvitationRepository
rect rgb(245,248,255)
note over C,Q: 그룹 초대 목록 (공통 페이징 사용)
C->>Q: GET /v1/groups/{id}/invitations/outgoing?paging params...
Q->>S: getOutgoingInvitations(userId, groupId, req)
S->>R: find... (req.getPageRequest())
R-->>S: Page<Invitation>
S-->>Q: PagingResponse<OutgoingGroupInvitationResponse>
Q-->>C: 200 OK
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. 📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (5)
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/main/java/project/flipnote/notification/service/NotificationService.java (1)
66-75: 커서 페이징 hasNext 판단이 항상 실패합니다 — over-fetch(+1) 적용 필요현 상태에선
req.getPageRequest()가 요청 사이즈만큼만 조회하고, 곧바로notifications.size() > req.getSize()로hasNext를 계산하므로hasNext는 항상 false가 됩니다. over-fetch(+1)로 한 건 더 조회한 뒤 잘라내는 패턴을 복원해야 합니다. 아래와 같이 서비스 레벨에서+1을 적용하는 수정을 제안합니다(요청 DTO는 “요청 값 그대로” 유지).다음 diff 적용 제안:
- List<Notification> notifications = notificationRepository.findNotificationsByReceiverIdAndCursor( - userId, req.getCursorId(), req.getGroupId(), req.getRead(), req.getPageRequest() - ); + PageRequest base = req.getPageRequest(); + PageRequest pageable = PageRequest.of(0, base.getPageSize() + 1, base.getSort()); + List<Notification> notifications = notificationRepository.findNotificationsByReceiverIdAndCursor( + userId, req.getCursorId(), req.getGroupId(), req.getRead(), pageable + );참고:
- 이후 로직(
size() > req.getSize()→subList(0, req.getSize()))은 그대로 동작합니다.id DESC정렬 고정(하위 요청 모델 오버라이드)은 커서 조건(n.id < :cursor)과 일관되어 적합합니다.src/main/java/project/flipnote/group/service/GroupInvitationService.java (1)
203-212: 광역 예외 삼키기 금지 — 최소한 경고 로그 남기기
catch (Exception ignored) {}는 문제 원인을 숨겨 장애 분석을 어렵게 합니다. 경고 로그를 남기고 다음 초대로 진행하세요.- } catch (Exception ignored) { - } + } catch (Exception e) { + log.warn("acceptPendingInvitationsOnRegister: failed to accept invitation id={} inviteeEmail={} - {}", + invitation.getId(), inviteeEmail, e.toString()); + }위 수정에는 로거가 필요합니다. 클래스 상단에
@Slf4j를 추가하고 import 해 주세요.// 추가 import import lombok.extern.slf4j.Slf4j; // 클래스 어노테이션에 추가 @Slf4j @Service public class GroupInvitationService { ... }
🧹 Nitpick comments (31)
src/main/java/project/flipnote/notification/model/NotificationListRequest.java (1)
13-23: 정렬 고정( id DESC )으로 상위 클래스 필드(sortBy/order)가 사실상 무시됨 — API 혼선 방지 권고본 클래스는
getPageRequest()를 오버라이드해 항상id DESC로 정렬합니다. 이는 커서 기준(id) 페이징의 일관성을 보장한다는 점에서 타당합니다. 다만 상위CursorPagingRequest의sortBy,order필드/게터가 그대로 노출되면 클라이언트가 전달한 값이 무시되어 혼선을 유발할 수 있습니다. 문서/스펙 상 “알림 목록은 정렬 불가, id DESC 고정”임을 명시하고, 필요 시 하위 클래스에서 게터를 오버라이드하여 스키마에서 숨기는 방식을 권장합니다.예시(클래스 내부에 추가):
// 필요한 경우: Swagger 문서에서 정렬 파라미터 비노출 @io.swagger.v3.oas.annotations.media.Schema(hidden = true) @Override public String getSortBy() { return "id"; } @io.swagger.v3.oas.annotations.media.Schema(hidden = true) @Override public String getOrder() { return "desc"; }src/main/java/project/flipnote/common/model/request/CursorPagingRequest.java (2)
45-59: getPageRequest 기본 정렬 정책 검토 제안(커서 기반과의 결합 고려)
sortBy가 비어있으면 정렬 없이PageRequest를 생성합니다. 이 클래스가 카드셋/알림 등 “id 커서”를 사용하는 엔드포인트에도 공통으로 쓰인다면, 기본 정렬을id DESC로 둘지, 각 하위 요청 모델에서 명시적으로 오버라이드(현재 알림처럼)할지 정책을 확정하는 게 안전합니다. 커서가 id에 묶여 있는데 정렬을 다른 필드로 받으면 페이지 경계와 커서가 불일치할 수 있습니다.
23-26: order를 문자열로 받는 대신 Enum/화이트리스트 사용 고려
order가 문자열이라 런타임 전까지 유효성 보장이 약합니다. Enum(ASC/DESC)으로 교체하거나@Pattern또는@Schema(allowableValues={"asc","desc"})로 제약을 명시하면 문서/검증 측면에서 견고해집니다.src/main/java/project/flipnote/common/model/response/PagingResponse.java (1)
19-31: 가독성: 파라미터 변수명page→springPage로 변경 제안로컬 변수명
page가 레코드 필드page와 중의적으로 읽힙니다. 혼동 최소화를 위해 파라미터명을 바꾸면 가독성이 좋아집니다.- public static <T> PagingResponse<T> from(Page<T> page) { - return new PagingResponse<>( - page.getContent(), - page.getNumber(), - page.getSize(), - page.getTotalElements(), - page.getTotalPages(), - page.isFirst(), - page.isLast(), - page.hasNext(), - page.hasPrevious() - ); + public static <T> PagingResponse<T> from(Page<T> springPage) { + return new PagingResponse<>( + springPage.getContent(), + springPage.getNumber(), + springPage.getSize(), + springPage.getTotalElements(), + springPage.getTotalPages(), + springPage.isFirst(), + springPage.isLast(), + springPage.hasNext(), + springPage.hasPrevious() + ); }src/main/java/project/flipnote/cardset/repository/CardSetRepository.java (1)
18-28: CardSetRepository JPQL 쿼리 개선 및 서비스 계층 키워드 전처리 추가 제안서비스 계층에서 전달된
keyword에 대해 트림(trim)이나 빈 문자열을null로 변환하는 전처리를 수행하고 있지 않음이 확인되었습니다. 따라서 아래 사항을 반영하면 검색 정확도 및 코드 의미 정합성을 동시에 높일 수 있습니다.– 주요 변경 포인트
• src/main/java/project/flipnote/cardset/service/CardSetService.java:117
java // 현재: req.getKeyword() 를 그대로 전달 Page<CardSet> CardSetPage = cardSetRepository.findByNameContainingAndCategory( req.getKeyword(), … );
• src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java:18
java private String keyword; // 별도 전처리 로직 없음
• src/main/java/project/flipnote/cardset/repository/CardSetRepository.java (Lines 18–28)– 제안된 쿼리 수정 예시
- @Query(""" - SELECT c FROM CardSet c - WHERE (:name IS NULL OR c.name LIKE CONCAT('%', :name, '%')) - AND (:category IS NULL OR c.category = :category) - AND c.publicVisible = TRUE - """) + @Query(""" + SELECT c FROM CardSet c + WHERE ( :name IS NULL + OR TRIM(:name) = '' + OR LOWER(c.name) LIKE CONCAT('%', LOWER(TRIM(:name)), '%') ) + AND ( :category IS NULL OR c.category = :category ) + AND c.publicVisible = TRUE + """) Page<CardSet> findByNameContainingAndCategory( @Param("name") String name, @Param("category") Category category, Pageable pageable );
TRIM(:name) = ''조건으로 빈 문자열을 미입력과 동일하게 처리LOWER(...)적용으로 대소문자 무시 검색 지원- 서비스 계층에서
req.getKeyword()이전에keyword = trimToNull(keyword)형태의 전처리를 해도 무방- 대규모 데이터셋 성능을 위해
countQuery명시도 추가 검토 권장src/main/java/project/flipnote/common/model/request/PagingRequest.java (1)
27-41: getPageRequest() 방어적 코딩: null/blank 입력 강건성 보강
sortBy의 공백,order의 null 등 예외 케이스 방어가 약합니다. 아래처럼 블랭크/널 처리와 인덱스 계산을 안전하게 하면 런타임 이슈를 줄일 수 있습니다.- if (sortBy == null || sortBy.isEmpty()) { - return PageRequest.of(page - 1, size); - } else { - Sort.Direction direction; - try { - direction = Sort.Direction.fromString(order); - } catch (IllegalArgumentException e) { - direction = Sort.Direction.DESC; - } - - return PageRequest.of(page - 1, size, Sort.by(direction, sortBy)); - } + final int safePage = (page == null ? 1 : page); + final int safeSize = (size == null ? 10 : size); + final int pageIndex = Math.max(safePage - 1, 0); + + if (sortBy == null || sortBy.isBlank()) { + return PageRequest.of(pageIndex, safeSize); + } else { + Sort.Direction direction; + try { + direction = Sort.Direction.fromString((order == null || order.isBlank()) ? "desc" : order); + } catch (IllegalArgumentException e) { + direction = Sort.Direction.DESC; + } + return PageRequest.of(pageIndex, safeSize, Sort.by(direction, sortBy)); + }src/main/java/project/flipnote/group/model/GroupInvitationListRequest.java (1)
10-13: 그룹 초대 목록 정렬 고정은 명확 — 상위 기본값 개선과 함께 안정성 확보
id DESC고정 정렬은 요구사항에 부합해 보입니다. 상위PagingRequest의page기본값/NPE 보완이 반영되면 이 구현도 함께 안정화됩니다. 현재 형태 유지에 동의합니다.선택사항: 재사용성 향상을 위해 다음처럼 상위 구현을 활용하는 방법도 있습니다.
@Override public PageRequest getPageRequest() { - return PageRequest.of(getPage() - 1, getSize(), Sort.by(Sort.Direction.DESC, "id")); + // 의도를 명시적으로 유지하면서 상위 처리 재사용 + setOrder("desc"); + setSortBy("id"); + return super.getPageRequest(); }src/main/java/project/flipnote/cardset/exception/CardSetErrorCode.java (2)
15-16: 비공개 리소스의 존재 노출(Enumeration) 방지 정책 점검 권고비공개 카드셋 접근 시 403을 반환하면 리소스 존재 여부가 유추될 수 있습니다. 보안 정책상 비인가 사용자에게는 404로 위장(Not Found)하는 패턴을 고려해 주세요. 만약 내부/멤버에게만 403을 주고 외부에는 404를 주는 이중 정책이라면, Policy/Controller 단에서 분기 구현이 필요합니다.
정책 의도가 "외부 사용자에게는 존재 자체 비노출"이 맞는지 확인 부탁드립니다. 맞다면 다음과 같이 정책 레이어에서 분기해 404/403을 선택적으로 반환하는 구현을 적용할 수 있습니다(의사 코드).
-throw new BizException(CardSetErrorCode.CARD_SET_PRIVATE); +if (!groupService.existsMember(groupId, userId)) { + throw new BizException(CardSetErrorCode.CARD_SET_NOT_FOUND); // 존재 위장 +} +throw new BizException(CardSetErrorCode.CARD_SET_PRIVATE); // 멤버이나 권한 없음
13-13: HTTP Status 일관성 제안: 그룹 멤버 미존재 403 → 404 정렬동일 의미의 오류가 Group 계열에선 404(USER_NOT_IN_GROUP)로 정의된 반면, 여기서는 403입니다. 클라이언트 처리 일관성을 위해 404로 정렬을 검토해 주세요.
적용 예시:
- GROUP_MEMBER_NOT_FOUND(HttpStatus.FORBIDDEN, "CARDSET_001", "해당 그룹의 멤버가 아닙니다."), + GROUP_MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "CARDSET_001", "해당 그룹의 멤버가 아닙니다."),src/main/java/project/flipnote/cardset/entity/CardSet.java (2)
62-68: 부분 업데이트(Partial Update) vs 전체 치환(PUT) 정책 명확화 필요—Null 덮어쓰기 위험현재 update는 payload 필드를 무조건 치환합니다. payload의 필드가 null이면 DB not null 컬럼(is_public, imageUrl 등)에 null을 기록하려다 예외가 발생할 수 있습니다. 부분 업데이트를 의도했다면 null 무시가 맞고, 전체 치환을 의도했다면 DTO 단 @NotNull 보증이 필요합니다.
부분 업데이트를 원하신다면 아래처럼 null 무시로 안전화해 주세요:
public void update(CardSetUpdatePayload payload) { - this.name = payload.name(); - this.publicVisible = payload.publicVisible(); - this.category = payload.category(); - this.hashtag = payload.hashtag(); - this.imageUrl = payload.imageUrl(); + if (payload.name() != null) this.name = payload.name(); + if (payload.publicVisible() != null) this.publicVisible = payload.publicVisible(); + if (payload.category() != null) this.category = payload.category(); + if (payload.hashtag() != null) this.hashtag = payload.hashtag(); + if (payload.imageUrl() != null) this.imageUrl = payload.imageUrl(); }전체 치환을 원한다면 CardSetUpdateRequest/CardSetUpdatePayload의 모든 필드에 @NotNull/@notblank 등을 부여했는지 확인 부탁드립니다. 미비 시 DTO에 제약 추가가 필요합니다.
46-49: 해시태그 스키마 표현 검토(문자열 vs 컬렉션)엔티티는 String, 생성 요청 DTO는 List입니다. 직렬화 규칙(예: CSV, JSON 문자열)과 정렬/중복 제거 정책을 명시하고 일관화하는 것을 권장합니다. 빈번한 검색/필터링이 있다면 정규화 테이블로의 분리도 고려 가치가 있습니다.
원하시면 마이그레이션 전략(테이블 분리 vs JSON 컬럼)과 쿼리 최적화 방안을 정리해 드리겠습니다.
src/main/java/project/flipnote/group/service/GroupService.java (1)
309-315: 주석(한글) 문구 오탈자 수정 제안자연스러운 표현으로 다듬으면 가독성이 올라갑니다.
- * 해당 회원에 그룹에 존재하는지 확인 + * 해당 회원이 그룹에 존재하는지 확인src/main/java/project/flipnote/cardset/model/CardSetSummaryResponse.java (2)
14-23: 목록 조회 시 N+1 가능성 점검: getGroup().getId() 지연 로딩CardSet 리스트를 매핑할 때 group 프록시 초기화로 N+1이 발생할 수 있습니다. 페이징 성능에 영향이 큽니다.
옵션:
- 리포지토리에서 그룹 ID를 함께 가져오도록 fetch join 또는 프로젝션 사용
- 곧바로 DTO 프로젝션으로 반환
예시(프로젝션 인터페이스):
public interface CardSetSummaryProjection { Long getCardSetId(); Long getGroupId(); String getName(); Category getCategory(); String getHashtag(); String getImageUrl(); }예시(JPQL DTO 프로젝션):
@Query(""" select new project.flipnote.cardset.model.CardSetSummaryResponse( c.id, g.id, c.name, c.category, c.hashtag, c.imageUrl ) from CardSet c join c.group g where ... """) Page<CardSetSummaryResponse> findCardSets(...);
9-10: 카테고리/해시태그 표현 통일성 제안
- category를 문자열(name)로 노출하지만 다른 DTO들에서 Enum 자체를 쓰는 경우가 있다면 일관화가 필요합니다.
- hashtag가 String이라면 클라이언트 파싱 규칙을 명시하거나 DTO에서 List으로 제공 후 서버에서 직렬화 방식을 통일하세요.
API 응답 스키마 표준안(카테고리 Enum 직렬화 방식, 해시태그 배열/문자열 정책)을 정리해 드릴 수 있습니다.
src/main/java/project/flipnote/cardset/model/CardSetDetailResponse.java (2)
14-15: 해시태그/카테고리 표현 방식 일관성 검토
- 요청 DTO(Create/Update)는
List<String> hashtag와Category(enum)를 받는데, 본 응답 DTO는String hashtag(콤마 연결 추정)와String category를 반환합니다. 클라이언트 입장에선 요청/응답 형식이 달라 파싱 로직이 중복되거나 상이해질 수 있습니다.권장 옵션(중 택1):
- 응답도
List<String> hashtags와Category category(또는String category유지 시에도 해시태그만 리스트로)로 맞춤- 혹은 현재 응답을 유지하되, 스웨거/문서에 콤마 연결 규칙을 명시
확인 요청:
- 카드셋 응답 요건이 이미 외부 계약으로 고정되었는지(브레이킹 체인지 위험 여부)
- 서버 내부 저장 포맷(DB)은 문자열이라도, 응답에서 리스트로 변환 가능 여부(성능/쿼리 영향 없음)
Also applies to: 31-32
18-22: 시간대 표기 명시 제안
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")만 설정되어 있어 타임존 오해 소지가 있습니다. 클라이언트와의 합의된 TZ(예: Asia/Seoul, UTC 등)가 있다면timezone속성 명시를 권장합니다.예:
- @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java (2)
19-20: 카테고리 타입 정합성(요청/도메인 간)검색 필드
category가String인데, 다른 DTO들(Create/Update) 및 도메인에서는Categoryenum을 사용합니다. 컨트롤러/서비스에서 다시 enum 변환이 필요하여 중복/실수 여지가 있습니다. Spring의 컨버전 기능을 사용해 enum으로 직접 바인딩하는 방식을 권장합니다.- private String category; + private Category category;변경 시:
- 리포지토리/서비스는
Category를 바로 사용 가능- 스웨거에서는 enum 값 목록이 자동 노출
16-16: 정렬 허용 필드 확장 고려(제품 요건 확인)현재
ALLOWED_SORT_FIELDS = {"id"}만 허용합니다. 목록 UX 관점에서createdAt/modifiedAt정렬 수요가 흔합니다. 제품 요건에 따라 허용 필드를 확장하거나 문서로 제한 사유를 명시해 주세요.src/main/java/project/flipnote/cardset/controller/docs/CardSetControllerDocs.java (1)
15-18: CardSetControllerDocs 인증 파라미터 일관성 검토CardSetControllerDocs의
getCardSets메서드에@SecurityRequirement(name = "access-token")만 선언되어 있고, 다른 Docs 인터페이스(User/Notification/GroupCardSet 등)처럼AuthPrinciple파라미터가 빠져 있습니다. 구현체(CardSetController)와 서비스(CardSetService)에서도 인증 정보를 받지 않아, Swagger 문서와 실제 시그니처 간 불일치가 발생합니다.검토 대상:
- src/main/java/project/flipnote/cardset/controller/docs/CardSetControllerDocs.java
- src/main/java/project/flipnote/cardset/controller/CardSetController.java
- src/main/java/project/flipnote/cardset/service/CardSetService.java
다음 중 한 가지를 선택해 일관성을 맞춰주세요.
AuthPrinciple파라미터 추가
· Docs 인터페이스, Controller, Service 시그니처에 모두AuthPrinciple authPrinciple을 추가
· Controller에서는@AuthenticationPrincipal어노테이션도 함께 지정
· Service 호출부를getCardSets(authPrinciple, req)등으로 변경// CardSetControllerDocs.java - ResponseEntity<PagingResponse<CardSetSummaryResponse>> getCardSets( - CardSetSearchRequest req - ); + ResponseEntity<PagingResponse<CardSetSummaryResponse>> getCardSets( + CardSetSearchRequest req, + AuthPrinciple authPrinciple + ); // CardSetController.java - public ResponseEntity<PagingResponse<CardSetSummaryResponse>> getCardSets( - @Valid @ModelAttribute CardSetSearchRequest req - ) { + public ResponseEntity<PagingResponse<CardSetSummaryResponse>> getCardSets( + @Valid @ModelAttribute CardSetSearchRequest req, + @AuthenticationPrincipal AuthPrinciple authPrinciple + ) { PagingResponse<CardSetSummaryResponse> res = cardSetService.getCardSets(req); return ResponseEntity.ok(res); } // CardSetService.java - public PagingResponse<CardSetSummaryResponse> getCardSets(CardSetSearchRequest req) { + public PagingResponse<CardSetSummaryResponse> getCardSets( + AuthPrinciple authPrinciple, + CardSetSearchRequest req + ) { // ... }
@SecurityRequirement제거
· 인증 정보가 필요 없는 공개 API라면 Docs 인터페이스에서security = {@SecurityRequirement(...)}만 삭제
· 별도 파라미터 변경 없이 Swagger 문서와 실제 동작 일치위 중 팀 컨벤션과 비즈니스 로직에 맞는 방안을 선택해 반영해주세요.
src/main/java/project/flipnote/cardset/model/CardSetUpdateRequest.java (2)
28-29: 필드명 일관성:image↔imageUrl다른 DTO/엔티티(
CardSetDetailResponse.imageUrl,CardSetUpdatePayload.imageUrl)와 네이밍이 엇갈립니다.imageUrl로 통일하면 의미가 명확해지고 혼동이 줄어듭니다. 변경 시 관련 매핑/문서도 함께 업데이트해 주세요.- @URL - String image + @URL + String imageUrl
15-17: 네이밍/검증 보강(선택):getHashTag→getHashtag, 정규화 로직 추가
- 메서드명은 일반적으로
getHashtag(소문자 h) 형태가 더 자연스럽습니다.- 저장 전에 각 항목 트리밍/중복 제거를 적용하면 데이터 정합성이 좋아집니다.
예시 구현:
- public String getHashTag() { - if (hashtag == null) return null; - return String.join(",", hashtag); - } + public String getHashtag() { + if (hashtag == null) return null; + return hashtag.stream() + .map(s -> s == null ? "" : s.trim()) + .filter(s -> !s.isEmpty()) + .distinct() + .reduce((a, b) -> a + "," + b) + .orElse(""); + }주의: 메서드명 변경 시
CardSetUpdatePayload.from()등 호출부도 함께 수정 필요합니다.Also applies to: 25-27, 32-35
src/main/java/project/flipnote/group/service/GroupInvitationPolicyService.java (2)
25-29: 권한 검증 로직을 정책 서비스로 일원화하세요.현재 GroupInvitationService.getOutgoingInvitations에서는 직접
groupService.hasPermission(...)를 호출하고 있습니다. 동일한 규칙을validateGroupInvitePermission(...)로 일원화하면 정책 변경 시 한 곳만 수정하면 되어 일관성과 유지보수성이 좋아집니다.
38-41: 이메일 자기초대 검증을 대소문자/공백 무시 및 null‑세이프하게 보강하세요.실무에서 이메일 비교는 대소문자 차이나 앞뒤 공백으로 오판단되는 경우가 잦습니다. 아래처럼
trim()+equalsIgnoreCase()와 null‑세이프 가드를 적용하는 편이 안전합니다.- public void validateSelfInvitation(String inviterUserEmail, String inviteeEmail) { - if (Objects.equals(inviterUserEmail, inviteeEmail)) { - throw new BizException(GroupInvitationErrorCode.CANNOT_INVITE_SELF); - } - } + public void validateSelfInvitation(String inviterUserEmail, String inviteeEmail) { + if (inviterUserEmail != null && inviteeEmail != null) { + if (inviterUserEmail.trim().equalsIgnoreCase(inviteeEmail.trim())) { + throw new BizException(GroupInvitationErrorCode.CANNOT_INVITE_SELF); + } + } + }src/main/java/project/flipnote/cardset/controller/docs/GroupCardSetControllerDocs.java (2)
14-14: 태그 명을 구분 가능하게 변경 제안기존 일반 카드셋 문서와 같은 태그명을 쓰면 스웨거 그룹이 섞일 수 있습니다. 그룹 스코프 API임을 드러내도록 태그명을 구분하면 탐색성이 좋아집니다.
-@Tag(name = "CardSet", description = "CardSet API") +@Tag(name = "Group CardSet", description = "Group CardSet API")
17-31: 스웨거 응답 스펙을 보완하여 클라이언트 합의 강화성공/실패 응답 코드와 스키마를 명시하면 API 소비자 경험이 좋아집니다. 예: 201(생성), 400(검증 실패), 401/403(권한), 404(그룹/카드셋 없음).
원하시면 @ApiResponses 템플릿을 생성해 드리겠습니다.
src/main/java/project/flipnote/cardset/service/CardSetPolicyService.java (1)
55-59: publicVisible가 null일 경우 NPE 가능성 — null‑세이프 체크 권장엔티티 컬럼이 NOT NULL이 아니라면 오토언박싱 시 NPE가 납니다. 아래와 같이 Boolean 비교를 명시적으로 처리하세요.
- if (!cardSet.getPublicVisible() && !groupService.existsMember(groupId, userId)) { + boolean isPublic = Boolean.TRUE.equals(cardSet.getPublicVisible()); + if (!isPublic && !groupService.existsMember(groupId, userId)) { throw new BizException(CardSetErrorCode.CARD_SET_PRIVATE); }src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java (2)
18-22: Swagger에서 페이징 파라미터가 펼쳐지도록 @ParameterObject 적용 제안springdoc를 사용 중이라면 PagingRequest 필드들이 쿼리 파라미터로 자동 전개되도록
@ParameterObject를 붙이는 편이 문서 품질에 도움이 됩니다. 구현체(Controller) 쪽에만 붙여도 되지만, 현재 Docs 인터페이스에 API 메타데이터를 모으는 패턴이므로 여기에도 붙이는 것을 권장합니다.아래처럼 수정하면 됩니다:
+import org.springdoc.core.annotations.ParameterObject; ResponseEntity<PagingResponse<OutgoingGroupInvitationResponse>> getOutgoingInvitations( Long groupId, - PagingRequest req, + @ParameterObject PagingRequest req, AuthPrinciple authPrinciple ); ResponseEntity<PagingResponse<IncomingGroupInvitationResponse>> getIncomingInvitations( - PagingRequest req, + @ParameterObject PagingRequest req, AuthPrinciple authPrinciple );Also applies to: 25-28
18-22: path 변수 문서화 보강(선택) — groupId 명시Docs 인터페이스에서
groupId가 path 변수임을 명시하면 Swagger UI에 더 명확히 드러납니다. 구현체에서 이미@PathVariable로 처리한다면 필수는 아니지만, 문서의 완결성을 위해 아래처럼 보강을 고려해 주세요.+import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; -ResponseEntity<PagingResponse<OutgoingGroupInvitationResponse>> getOutgoingInvitations( - Long groupId, +ResponseEntity<PagingResponse<OutgoingGroupInvitationResponse>> getOutgoingInvitations( + @Parameter(in = ParameterIn.PATH, name = "groupId", description = "그룹 ID") Long groupId, @ParameterObject PagingRequest req, AuthPrinciple authPrinciple );src/main/java/project/flipnote/cardset/service/CardSetService.java (2)
47-49: 그룹 멤버십 검증 경로 단일화 및 불필요 의존성 제거현재
existGroupMember()가GroupMemberRepository를 직접 사용합니다. 동일 책임을 가진GroupService.existsMember(...)가 이미 존재하므로 서비스 계층을 통해 일관되게 검증하는 편이 좋습니다. 이렇게 하면 Repository 직접 의존을 줄이고 정책 변경 시 영향 범위를 축소할 수 있습니다. 동시에GroupMemberRepository필드와existGroupMember()메서드는 제거 가능합니다.아래와 같이 정리 제안:
@@ - private final GroupMemberRepository groupMemberRepository; @@ - private boolean existGroupMember(Group group, UserProfile user) { - return groupMemberRepository.existsByGroup_idAndUser_id((group.getId()), user.getId()); - } @@ - //그룹 내 유저 있는지 확인 - if (!existGroupMember(group, user)) { + //그룹 내 유저 있는지 확인 + if (!groupService.existsMember(groupId, user.getId())) { throw new BizException(CardSetErrorCode.GROUP_MEMBER_NOT_FOUND); }Also applies to: 62-64, 75-78
162-163: saveAndFlush 불필요 플러시트랜잭션 커밋 시점에 플러시되므로 특별한 이유가 없다면
save()로 충분합니다. 빈번한flush는 불필요한 DB round-trip을 유발할 수 있습니다.- cardSetRepository.saveAndFlush(cardSet); + cardSetRepository.save(cardSet);src/main/java/project/flipnote/cardset/controller/GroupCardSetController.java (1)
31-40: 불필요한 빈 경로 제거로 매핑 명확화
@PostMapping("")는@PostMapping과 동일합니다. 빈 문자열 경로는 관례상 생략하는 편이 명확하고, 다른 컨트롤러들과의 스타일도 일치합니다.- @PostMapping("") + @PostMapping
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (28)
src/main/java/project/flipnote/cardset/controller/CardSetController.java(1 hunks)src/main/java/project/flipnote/cardset/controller/GroupCardSetController.java(1 hunks)src/main/java/project/flipnote/cardset/controller/docs/CardSetControllerDocs.java(1 hunks)src/main/java/project/flipnote/cardset/controller/docs/GroupCardSetControllerDocs.java(1 hunks)src/main/java/project/flipnote/cardset/entity/CardSet.java(2 hunks)src/main/java/project/flipnote/cardset/exception/CardSetErrorCode.java(1 hunks)src/main/java/project/flipnote/cardset/model/CardSetDetailResponse.java(1 hunks)src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java(1 hunks)src/main/java/project/flipnote/cardset/model/CardSetSummaryResponse.java(1 hunks)src/main/java/project/flipnote/cardset/model/CardSetUpdatePayload.java(1 hunks)src/main/java/project/flipnote/cardset/model/CardSetUpdateRequest.java(1 hunks)src/main/java/project/flipnote/cardset/repository/CardSetManagerRepository.java(1 hunks)src/main/java/project/flipnote/cardset/repository/CardSetRepository.java(1 hunks)src/main/java/project/flipnote/cardset/service/CardSetPolicyService.java(1 hunks)src/main/java/project/flipnote/cardset/service/CardSetService.java(4 hunks)src/main/java/project/flipnote/common/model/request/CursorPagingRequest.java(3 hunks)src/main/java/project/flipnote/common/model/request/PagingRequest.java(1 hunks)src/main/java/project/flipnote/common/model/response/PagingResponse.java(2 hunks)src/main/java/project/flipnote/group/controller/GroupController.java(0 hunks)src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java(2 hunks)src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java(2 hunks)src/main/java/project/flipnote/group/entity/Category.java(1 hunks)src/main/java/project/flipnote/group/model/GroupInvitationListRequest.java(1 hunks)src/main/java/project/flipnote/group/service/GroupInvitationPolicyService.java(1 hunks)src/main/java/project/flipnote/group/service/GroupInvitationService.java(7 hunks)src/main/java/project/flipnote/group/service/GroupService.java(2 hunks)src/main/java/project/flipnote/notification/model/NotificationListRequest.java(1 hunks)src/main/java/project/flipnote/notification/service/NotificationService.java(1 hunks)
💤 Files with no reviewable changes (1)
- src/main/java/project/flipnote/group/controller/GroupController.java
🧰 Additional context used
🧬 Code graph analysis (25)
src/main/java/project/flipnote/cardset/model/CardSetUpdatePayload.java (1)
src/main/java/project/flipnote/cardset/model/CreateCardSetRequest.java (1)
CreateCardSetRequest(12-30)
src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java (2)
src/main/java/project/flipnote/common/model/request/PagingRequest.java (1)
Getter(12-42)src/main/java/project/flipnote/notification/model/NotificationListRequest.java (1)
Getter(11-24)
src/main/java/project/flipnote/group/model/GroupInvitationListRequest.java (2)
src/main/java/project/flipnote/group/entity/GroupInvitation.java (1)
Getter(26-104)src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java (1)
GroupInvitationRepository(17-43)
src/main/java/project/flipnote/notification/model/NotificationListRequest.java (3)
src/main/java/project/flipnote/common/model/request/CursorPagingRequest.java (1)
Getter(13-60)src/main/java/project/flipnote/notification/repository/NotificationRepository.java (2)
NotificationRepository(15-45)Query(17-30)src/main/java/project/flipnote/common/model/request/CursorPageRequest.java (1)
Getter(11-38)
src/main/java/project/flipnote/cardset/model/CardSetSummaryResponse.java (2)
src/main/java/project/flipnote/cardset/model/CreateCardSetResponse.java (2)
CreateCardSetResponse(4-10)from(7-9)src/main/java/project/flipnote/group/model/GroupDetailResponse.java (1)
GroupDetailResponse(8-41)
src/main/java/project/flipnote/cardset/entity/CardSet.java (1)
src/main/java/project/flipnote/cardset/model/CreateCardSetRequest.java (1)
CreateCardSetRequest(12-30)
src/main/java/project/flipnote/group/service/GroupService.java (3)
src/test/java/project/flipnote/group/service/GroupServiceTest.java (2)
Test(326-360)Test(405-436)src/main/java/project/flipnote/group/repository/GroupMemberRepository.java (2)
existsByGroup_IdAndUser_Id(28-28)existsByGroup_idAndUser_id(20-20)src/main/java/project/flipnote/group/service/GroupMemberPolicyService.java (1)
RequiredArgsConstructor(22-69)
src/main/java/project/flipnote/group/entity/Category.java (2)
src/main/java/project/flipnote/auth/entity/AccountRole.java (1)
AccountRole(5-14)src/main/java/project/flipnote/group/model/GroupInvitationStatus.java (1)
from(8-10)
src/main/java/project/flipnote/cardset/controller/docs/CardSetControllerDocs.java (3)
src/main/java/project/flipnote/user/controller/docs/UserControllerDocs.java (1)
Tag(14-28)src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java (1)
Tag(13-28)src/main/java/project/flipnote/notification/controller/docs/NotificationControllerDocs.java (1)
Tag(13-30)
src/main/java/project/flipnote/cardset/controller/GroupCardSetController.java (4)
src/main/java/project/flipnote/cardset/controller/CardSetController.java (1)
RequiredArgsConstructor(17-32)src/main/java/project/flipnote/group/controller/GroupInvitationController.java (1)
RequiredArgsConstructor(23-63)src/main/java/project/flipnote/groupjoin/controller/GroupJoinController.java (1)
RestController(14-76)src/test/java/project/flipnote/cardset/service/CardSetServiceTest.java (1)
cardSetService(117-117)
src/main/java/project/flipnote/common/model/request/PagingRequest.java (2)
src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java (1)
Getter(12-35)src/main/java/project/flipnote/common/model/request/CursorPagingRequest.java (1)
Getter(13-60)
src/main/java/project/flipnote/notification/service/NotificationService.java (2)
src/main/java/project/flipnote/notification/repository/NotificationRepository.java (4)
NotificationRepository(15-45)Query(17-30)Modifying(32-42)findByIdAndReceiverId(44-44)src/main/java/project/flipnote/notification/controller/NotificationController.java (1)
GetMapping(31-40)
src/main/java/project/flipnote/cardset/repository/CardSetManagerRepository.java (4)
src/main/java/project/flipnote/cardset/entity/CardSetManager.java (1)
Getter(18-45)src/main/java/project/flipnote/groupjoin/repository/GroupJoinRepository.java (1)
existsByGroup_idAndUser_id(18-18)src/main/java/project/flipnote/group/repository/GroupMemberRepository.java (2)
existsByGroup_idAndUser_id(20-20)existsByGroup_IdAndUser_Id(28-28)src/main/java/project/flipnote/group/repository/GroupRepository.java (1)
existsByIdAndDeletedAtIsNull(26-26)
src/main/java/project/flipnote/cardset/controller/docs/GroupCardSetControllerDocs.java (2)
src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java (3)
Tag(13-28)Operation(16-19)Operation(24-27)src/main/java/project/flipnote/user/controller/docs/UserControllerDocs.java (1)
Tag(14-28)
src/main/java/project/flipnote/cardset/model/CardSetUpdateRequest.java (2)
src/main/java/project/flipnote/cardset/model/CreateCardSetRequest.java (1)
CreateCardSetRequest(12-30)src/main/java/project/flipnote/user/model/UserUpdateRequest.java (1)
UserUpdateRequest(8-25)
src/main/java/project/flipnote/group/service/GroupInvitationPolicyService.java (1)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (1)
RequiredArgsConstructor(34-273)
src/main/java/project/flipnote/cardset/service/CardSetService.java (3)
src/main/java/project/flipnote/cardset/controller/GroupCardSetController.java (1)
RequiredArgsConstructor(24-65)src/main/java/project/flipnote/cardset/service/CardSetPolicyService.java (1)
RequiredArgsConstructor(13-60)src/main/java/project/flipnote/cardset/controller/CardSetController.java (1)
RequiredArgsConstructor(17-32)
src/main/java/project/flipnote/cardset/exception/CardSetErrorCode.java (3)
src/main/java/project/flipnote/group/exception/GroupErrorCode.java (1)
Getter(9-29)src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java (1)
Getter(9-25)src/main/java/project/flipnote/auth/exception/AuthErrorCode.java (1)
Getter(9-36)
src/main/java/project/flipnote/cardset/service/CardSetPolicyService.java (1)
src/main/java/project/flipnote/cardset/controller/GroupCardSetController.java (1)
RequiredArgsConstructor(24-65)
src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java (2)
src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java (2)
Tag(13-28)Operation(24-27)src/main/java/project/flipnote/group/controller/GroupInvitationController.java (1)
RequiredArgsConstructor(23-63)
src/main/java/project/flipnote/cardset/controller/CardSetController.java (2)
src/main/java/project/flipnote/cardset/controller/GroupCardSetController.java (1)
RequiredArgsConstructor(24-65)src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java (1)
RequiredArgsConstructor(21-50)
src/main/java/project/flipnote/cardset/model/CardSetDetailResponse.java (3)
src/main/java/project/flipnote/cardset/model/CreateCardSetResponse.java (1)
CreateCardSetResponse(4-10)src/main/java/project/flipnote/group/model/OutgoingGroupInvitationResponse.java (1)
OutgoingGroupInvitationResponse(9-32)src/main/java/project/flipnote/group/model/IncomingGroupInvitationResponse.java (1)
IncomingGroupInvitationResponse(9-26)
src/main/java/project/flipnote/common/model/response/PagingResponse.java (1)
src/main/java/project/flipnote/common/model/response/CursorPageResponse.java (3)
CursorPageResponse(6-21)of(13-15)of(17-20)
src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java (2)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (1)
RequiredArgsConstructor(34-273)src/main/java/project/flipnote/cardset/controller/CardSetController.java (1)
RequiredArgsConstructor(17-32)
src/main/java/project/flipnote/common/model/request/CursorPagingRequest.java (3)
src/main/java/project/flipnote/common/model/response/CursorPageResponse.java (3)
CursorPageResponse(6-21)of(13-15)of(17-20)src/main/java/project/flipnote/common/model/response/PageResponse.java (1)
PageResponse(7-32)src/main/java/project/flipnote/notification/repository/NotificationRepository.java (1)
NotificationRepository(15-45)
🔇 Additional comments (27)
src/main/java/project/flipnote/notification/model/NotificationListRequest.java (1)
3-4: 필요한 의존성 임포트 적절
PageRequest,Sort임포트는 본 클래스 오버라이드에서 사용되며 타당합니다.src/main/java/project/flipnote/common/model/request/CursorPagingRequest.java (1)
3-4: PageRequest/Sort 임포트 추가 적절
getPageRequest()신설에 따른 임포트는 타당합니다.src/main/java/project/flipnote/group/entity/Category.java (2)
4-4: 세미콜론 추가로 enum 메서드 정의 가능 상태 확보 — 좋습니다상수 목록 뒤 세미콜론 추가로 정적 메서드 정의가 가능해졌습니다. 변경 방향 타당합니다.
6-15: Category.from 호출 사용 확인 및 null 반환 안전
Category.from(String)메서드는CardSetService.java118행에서만 호출되고 있습니다.
null 반환값은 그대로 JPA 메서드findByNameContainingAndCategory(req.getKeyword(), null, req.getPageRequest())로 전달되며, 현재 코드 흐름상 예외나 NPE 없이 안전하게 처리됩니다.따라서 제안드린
- return Category.valueOf(category); + String normalized = category.trim(); + for (Category c : Category.values()) { + if (c.name().equalsIgnoreCase(normalized)) { + return c; + } + } + return null;와 같은 공백 트림 및 대소문자 무시 처리 리팩토링은 기존 동작을 변경하지 않으면서 신뢰성과 일관성을 높이는 Optional Refactor로 적용하셔도 무방합니다.
src/main/java/project/flipnote/common/model/response/PagingResponse.java (1)
7-17: Paging 메타데이터 구성 적절
first/last/hasNext/hasPrevious까지 포함한 응답 포맷 구성은 직관적이고 확장성 좋습니다. 현 상태 유지에 동의합니다.src/main/java/project/flipnote/cardset/repository/CardSetRepository.java (1)
30-31: 그룹 경로 제한용 조회 메서드 적절권한/정책 체크 용도로
findByIdAndGroup_Id추가 적절합니다.src/main/java/project/flipnote/cardset/exception/CardSetErrorCode.java (1)
13-16: 카드셋 도메인 전용 에러코드 확장, 방향성 좋습니다권한/존재/비공개 케이스가 명확히 분리되어 컨트롤러/정책 레이어에서 해석이 쉬워졌습니다.
src/main/java/project/flipnote/cardset/repository/CardSetManagerRepository.java (1)
11-11: 호출부 및 테스트 영향 범위 점검 필요
파생 쿼리명 변경(existsByIdAndCardSet_Id)으로 인해 잘못된 결과가 반환될 수 있으므로, 아래 호출부와 테스트 커버리지를 꼭 확인해 주세요.• 호출부
src/main/java/project/flipnote/cardset/service/CardSetPolicyService.java42행:if (!cardSetManagerRepository.existsByIdAndCardSet_Id(userId, cardSetId)) { … }src/main/java/project/flipnote/cardset/service/CardSetService.java157행:cardSetPolicyService.validateCardSetEditable(userId, cardSetId);• 올바른 메서드명 후보
existsByUser_IdAndCardSet_Id(Long userId, Long cardSetId)existsByCardSet_IdAndUser_Id(Long userId, Long cardSetId)• 테스트 검증
CardSetPolicyService및CardSetService관련 단위/통합 테스트에서 위 로직이 실제로 실행되고, 올바른 사용자·카드셋 권한 여부를 검증하는 시나리오가 포함되어 있는지 확인해 주세요.src/main/java/project/flipnote/group/service/GroupService.java (2)
63-63: 사소한 스타일 변경(무영향)조건문 포맷 변경만 있으며 기능상 변화는 없습니다.
308-318: existsMember 도입으로 권한/정책 레이어 재사용성 향상여러 서비스(예: 카드셋 정책)에서 그룹 멤버십 검증을 공용 메서드로 호출할 수 있어 응집도가 좋아졌습니다. 트랜잭션 readOnly 컨텍스트에도 부합합니다.
src/main/java/project/flipnote/cardset/model/CardSetSummaryResponse.java (1)
5-12: DTO 구성 적절 — 목록 응답에 필요한 최소 필드만 노출id·이름·카테고리·썸네일·그룹 식별자만 담아 목록 뷰 최적화에 적합합니다.
src/main/java/project/flipnote/cardset/model/CardSetDetailResponse.java (1)
28-28: 해결:group매핑은 기본 EAGER Fetch로 설정되어 있어 Lazy 초기화 예외 및 NPE 우려가 없습니다.
CardSet엔티티의매핑은@ManyToOne @JoinColumn(name = "group_id", nullable = false) private Group group;fetch속성을 지정하지 않아 JPA 기본값인FetchType.EAGER가 적용됩니다. 따라서cardSetRepository.findByIdAndGroup_Id(...)호출 시 즉시 그룹 엔티티가 로드됩니다.CardSetService클래스는@Transactional(readOnly = true)가 클래스 레벨에 선언되어 있어, 조회 시점에 열린 영속성 컨텍스트 내에서group.getId()접근이 안전하게 이루어집니다.@JoinColumn(nullable = false)로 DB 차원에서도group_id가 항상 존재함을 보장하므로,cardSet.getGroup()이null을 반환할 가능성도 없습니다.위 검증 내용을 바탕으로, 추가적인 fetch-join이나 projection 전용 메서드 작성 없이도 해당 구문은 안전합니다.
src/main/java/project/flipnote/cardset/controller/docs/GroupCardSetControllerDocs.java (3)
17-21: 생성 API 시그니처와 구현 컨트롤러 정합성 확인 — OK파라미터 순서(인증, groupId, req)가 구현체(GroupCardSetController)와 일치합니다.
22-24: 상세 조회 API 시그니처 정합성 — OKgroupId, cardSetId, 인증 객체 순서가 구현과 일치합니다.
25-31: 수정 API 시그니처 정합성 — OK파라미터 순서 및 반환타입(ResponseEntity)이 구현과 일치합니다.
src/main/java/project/flipnote/cardset/controller/CardSetController.java (1)
24-31: 검색 파라미터를 @ModelAttribute로 바인딩하는 GET 목록 API 구조 — OKPagingResponse 래핑과 서비스 위임이 명확합니다. 인증이 필요 없는 퍼블릭 검색이라면 현재 구성이 적합합니다.
src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java (3)
29-36: 보낸 초대 목록: 페이징 모델 전환 — OK
PagingRequest를 @ModelAttribute로 받도록 정리되었고, 서비스 호출 시 userId/groupId/req 전달도 일치합니다.
41-46: 받은 초대 목록: 페이징 모델 전환 — OK일관된 패턴(@Valid @ModelAttribute)으로 잘 정리되어 있습니다.
31-31: PagingRequest 제약 검증 확인 완료PagingRequest 클래스에 이미 아래와 같이 페이지와 사이즈 필드에 최소/최대 제약이 정의되어 있어 추가 작업이 필요하지 않습니다:
page필드에@Min(1)적용size필드에@Min(1)및@Max(30)적용검증 스크립트 결과:
16: @Min(1) // page 19: @Min(1) // size 20: @Max(30) // size해당 제약이 유효하므로, 컨트롤러의
@Valid @ModelAttribute PagingRequest req사용 시 의도한 대로 검증이 수행됩니다.src/main/java/project/flipnote/cardset/service/CardSetPolicyService.java (1)
29-32: 그룹/ID로 조회: 예외 전파 방식 — OK존재하지 않을 때 전용 에러코드로 BizException을 던지는 방식이 서비스 계층에서 재사용하기 좋습니다.
src/main/java/project/flipnote/cardset/service/CardSetService.java (1)
36-40: 클래스 레벨 readOnly 기본값 적절, 쓰기 메서드에 @transactional 적용 👍쓰기 메서드(
createCardSet,updateCardSet)에서@Transactional로 readOnly 기본값을 적절히 무시하고 있습니다. 트랜잭션 경계 설정이 명확합니다.src/main/java/project/flipnote/cardset/controller/GroupCardSetController.java (2)
42-51: 상세 조회 엔드포인트 흐름 일치 — 정책 서비스 연계 OK인증 정보에서
userId를 추출하여 서비스의 정책 검증 흐름(validateCardSetViewable)으로 위임하는 구성이 일관되고 적절합니다.
53-63: 수정 엔드포인트 동작 및 검증 플로우 적절
@Valid검증, 정책 기반 수정 권한 체크, 수정 결과 반환까지 플로우가 깔끔합니다.src/main/java/project/flipnote/group/service/GroupInvitationService.java (4)
63-68: 초대 권한/자기 초대 방지 검증을 정책 서비스로 위임 — 책임 분리가 좋아졌습니다
GroupInvitationPolicyService로 권한 및 자기 초대 방지 검증을 이관하여 서비스 메서드가 간결해졌습니다.
84-93: PENDING 상태에 한해 취소 허용 — 비즈니스 규칙 명확
PENDING조건으로 조회 후 삭제하는 로직이 명확하며, 예외 코드도 적절합니다.
141-168: 페이징 통합과 닉네임 배치 조회 👍 (성능 TODO 인지)
PagingRequest/PagingResponse통합과 invitee 닉네임 배치 조회로 N+1을 회피하는 방향이 적절합니다. Projection/카운트 튜닝 TODO도 명시되어 있어 추후 최적화 포인트가 분명합니다.
178-185: 수신 초대 페이징 조회 흐름 간결하고 일관
PagingRequest로 조회하고PagingResponse로 감싸는 패턴이 송신 조회와 일관됩니다.
src/main/java/project/flipnote/cardset/model/CardSetDetailResponse.java
Outdated
Show resolved
Hide resolved
src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java
Outdated
Show resolved
Hide resolved
src/main/java/project/flipnote/cardset/repository/CardSetManagerRepository.java
Outdated
Show resolved
Hide resolved
src/main/java/project/flipnote/cardset/service/CardSetPolicyService.java
Show resolved
Hide resolved
src/main/java/project/flipnote/common/model/request/CursorPagingRequest.java
Show resolved
Hide resolved
src/main/java/project/flipnote/notification/model/NotificationListRequest.java
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (2)
src/main/java/project/flipnote/cardset/repository/CardSetManagerRepository.java (1)
11-11: 중대 버그 수정 완료: 파생 쿼리를 user.id/cardSet.id로 교정 — LGTM이전 코멘트에서 지적된 Id vs User.Id 혼동이 해소되었습니다. CardSetManager.user.id와 cardSet.id를 정확히 타고 내려가므로 의도한 “특정 사용자가 해당 카드셋의 매니저인지” 존재 여부를 올바르게 판정합니다.
src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java (1)
21-34: NPE 및 잘못된 정렬 파싱 가능성: page/size/order null 케이스 방어 필요
- getPage()/getSize()가 null이면 산술 연산에서 NPE가 발생합니다. (이전 리뷰에서도 동일 이슈 제기됨)
- order가 null일 경우 Sort.Direction.fromString(...)이 IllegalArgumentException가 아닌 NPE가 날 수 있습니다(스프링 버전에 따라). null/빈문자 입력은 DESC로 안전하게 폴백하세요.
- sortBy의 공백/대소문자 입력을 정규화(trim/lowercase)하면 예기치 않은 미스매치를 줄일 수 있습니다.
아래처럼 메서드를 방어적으로 갱신하는 것을 권장합니다.
@@ @Override public PageRequest getPageRequest() { - String sortBy = this.getSortBy(); - String effectiveSortBy = (sortBy != null && ALLOWED_SORT_FIELDS.contains(sortBy)) ? sortBy : "id"; - - Sort.Direction direction; - try { - direction = Sort.Direction.fromString(this.getOrder()); - } catch (IllegalArgumentException e) { - direction = Sort.Direction.DESC; - } - - return PageRequest.of(getPage() - 1, getSize() + 1, Sort.by(direction, effectiveSortBy)); + // 1) sortBy: whitelist + 공백/대소문자 정규화 + String rawSortBy = this.getSortBy(); + String normalizedSortBy = (rawSortBy == null) ? null : rawSortBy.trim().toLowerCase(); + String effectiveSortBy = (normalizedSortBy != null && ALLOWED_SORT_FIELDS.contains(normalizedSortBy)) + ? normalizedSortBy : "id"; + + // 2) order: null/invalid 안전 기본값 DESC + Sort.Direction direction = Sort.Direction.DESC; + String rawOrder = this.getOrder(); + if (rawOrder != null && !rawOrder.isBlank()) { + try { + direction = Sort.Direction.fromString(rawOrder); + } catch (IllegalArgumentException ignored) { + // keep DESC + } + } + + // 3) page/size: null/음수 방어, 기본값 1/10 유지 + Integer page = this.getPage(); + Integer size = this.getSize(); + int safePage = (page == null || page < 1) ? 1 : page; + int safeSize = (size == null || size < 1) ? 10 : size; + + return PageRequest.of(safePage - 1, safeSize + 1, Sort.by(direction, effectiveSortBy)); }
🧹 Nitpick comments (9)
src/main/java/project/flipnote/cardset/repository/CardSetManagerRepository.java (2)
9-12: 레포지토리 간 네이밍/인자 순서 일관화 제안GroupMemberRepository에는
existsByGroup_IdAndUser_Id형태가 존재합니다. 도메인 우선(리소스→주체) 순서를 택한다면 카드셋도existsByCardSet_IdAndUser_Id(cardSetId, userId)로 맞추는 편이 가독성에 유리합니다. 현재 것도 동작상 문제는 없으므로, 일단 병행 제공 후 코드베이스 정리 시 하나로 통일하는 방식을 권장합니다.가능한 보강안(현 메서드는 유지, 별도 별칭 추가):
public interface CardSetManagerRepository extends JpaRepository<CardSetManager, Long> { - boolean existsByUser_IdAndCardSet_Id(Long userId, Long cardSetId); + boolean existsByUser_IdAndCardSet_Id(Long userId, Long cardSetId); + // 도메인 우선 네이밍(선호 스타일 정착 시, 추후 한쪽만 남기는 방향 권장) + boolean existsByCardSet_IdAndUser_Id(Long cardSetId, Long userId); }참고: 동일 시그니처(Long, Long)라서 “이름만 같은 오버로드”는 불가하므로, 위와 같이 “다른 이름” 두 개를 병행하는 접근이 안전합니다.
8-9: @repository 애노테이션은 선택사항 — 제거 고려(선택)Spring Data JPA는 프록시 생성 시 Repository 예외 변환을 내장하므로 인터페이스에 @repository를 명시하지 않아도 동작합니다. 팀 컨벤션에 따라 유지 또는 제거를 결정하세요. 제거 시 표면적 잡음을 줄일 수 있습니다.
-@Repository public interface CardSetManagerRepository extends JpaRepository<CardSetManager, Long> {src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java (4)
18-20: category는 String 대신 Enum 바인딩 고려API 레벨에서 Enum으로 직접 바인딩하면 다음 이점이 있습니다:
- 유효하지 않은 값은 자동으로 400 처리(컨트롤러 레벨에서 변환/검증 코드 감소)
- Swagger/OpenAPI에 허용 가능한 값 목록이 노출되어 클라이언트 사용성 향상
- 다운스트림(Service/Repository) 변환 로직 제거
도메인 계층의 Category enum을 직접 노출하기 어렵다면, API 전용 Enum을 모델 패키지에 두고 내부 도메인 Enum과 매핑하는 방식을 추천합니다.
18-19: keyword 입력 정규화(공백만 입력 등) 처리 제안검색 키워드의 좌우 공백 제거 정도는 DTO 레벨에서 흡수해두면 호출부 간 일관성이 좋아집니다. Lombok이 세터를 생성하더라도 동일 시그니처의 명시적 세터를 추가하면 Lombok이 생성하지 않습니다.
@@ private String keyword; private String category; + // keyword: 공백만 전달되는 경우를 대비해 trim + public void setKeyword(String keyword) { + this.keyword = (keyword == null) ? null : keyword.trim(); + }
23-25: 정렬 필드 비교 시 대소문자/공백 정규화 권장API 파라미터 "ID", " id " 등을 허용하려면 현재 contains 검사를 하기 전에 trim/lowercase 처리 필요합니다. 위의 getPageRequest() 수정안에 포함되어 있습니다.
1-15: 간단 테스트 케이스 추가 제안
- page=null, size=null, order=null, sortBy="ID" → PageRequest(0, 11, Sort.by(DESC, "id")) 생성 확인
- order="ascending"(오타) → DESC 폴백 확인
- sortBy="name"(미허용) → "id" 폴백 확인
필요하면 단위 테스트 스켈레톤 제공하겠습니다.
src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java (2)
31-31:@ModelAttribute대신@ParameterObject사용을 고려해보세요
GroupInvitationListRequest는 쿼리 파라미터를 받는 DTO로 보입니다. Spring Boot 3.x와 SpringDoc OpenAPI에서는 Swagger 문서 생성을 위해@ParameterObject어노테이션 사용을 권장합니다.- @Valid @ModelAttribute GroupInvitationListRequest req, + @Valid @ParameterObject GroupInvitationListRequest req,추가로 필요한 import:
import org.springdoc.core.annotations.ParameterObject;
42-42: 일관성을 위해 여기도@ParameterObject사용을 권장합니다위 메서드와 동일하게
@ParameterObject어노테이션 사용을 권장합니다.- @Valid @ModelAttribute GroupInvitationListRequest req, + @Valid @ParameterObject GroupInvitationListRequest req,src/main/java/project/flipnote/group/service/GroupInvitationService.java (1)
150-152: TODO 주석: Projection 및 카운트 쿼리 최적화 필요성능 최적화가 필요하다고 표시되어 있습니다. N+1 문제나 불필요한 카운트 쿼리가 발생할 수 있으니 추후 개선이 필요합니다.
Projection을 사용한 쿼리 최적화나 카운트 쿼리 튜닝에 대한 구체적인 개선 방안을 제안해드릴까요? 새로운 이슈를 생성하시겠습니까?
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (10)
src/main/java/project/flipnote/cardset/model/CardSetDetailResponse.java(1 hunks)src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java(1 hunks)src/main/java/project/flipnote/cardset/model/CardSetUpdateRequest.java(1 hunks)src/main/java/project/flipnote/cardset/repository/CardSetManagerRepository.java(1 hunks)src/main/java/project/flipnote/cardset/service/CardSetPolicyService.java(1 hunks)src/main/java/project/flipnote/common/model/request/PagingRequest.java(1 hunks)src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java(2 hunks)src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java(1 hunks)src/main/java/project/flipnote/group/model/GroupInvitationListRequest.java(1 hunks)src/main/java/project/flipnote/group/service/GroupInvitationService.java(8 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
- src/main/java/project/flipnote/group/model/GroupInvitationListRequest.java
- src/main/java/project/flipnote/common/model/request/PagingRequest.java
- src/main/java/project/flipnote/cardset/model/CardSetUpdateRequest.java
- src/main/java/project/flipnote/cardset/service/CardSetPolicyService.java
- src/main/java/project/flipnote/cardset/model/CardSetDetailResponse.java
🧰 Additional context used
🧬 Code graph analysis (5)
src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java (2)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (1)
RequiredArgsConstructor(34-276)src/main/java/project/flipnote/group/model/GroupInvitationListRequest.java (1)
GroupInvitationListRequest(8-14)
src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java (2)
src/main/java/project/flipnote/common/model/request/PagingRequest.java (1)
Getter(12-42)src/main/java/project/flipnote/cardset/entity/CardSet.java (1)
Getter(21-60)
src/main/java/project/flipnote/cardset/repository/CardSetManagerRepository.java (3)
src/main/java/project/flipnote/cardset/entity/CardSetManager.java (1)
Getter(18-45)src/main/java/project/flipnote/groupjoin/repository/GroupJoinRepository.java (1)
existsByGroup_idAndUser_id(18-18)src/main/java/project/flipnote/group/repository/GroupMemberRepository.java (2)
existsByGroup_idAndUser_id(20-20)existsByGroup_IdAndUser_Id(28-28)
src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java (1)
src/main/java/project/flipnote/group/model/GroupInvitationListRequest.java (1)
GroupInvitationListRequest(8-14)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (1)
src/main/java/project/flipnote/group/model/GroupInvitationListRequest.java (1)
GroupInvitationListRequest(8-14)
🔇 Additional comments (7)
src/main/java/project/flipnote/cardset/repository/CardSetManagerRepository.java (1)
11-11: 레포지토리 메서드 호출 검증 완료스크립트 실행 결과 기존
existsByIdAndCardSet_Id(…)호출은 모두 제거되었으며, 새 메서드existsByUser_IdAndCardSet_Id(userId, cardSetId)호출부의 파라미터 순서도 모두 올바르게 유지되고 있습니다. 추가 조치가 필요하지 않습니다.src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java (1)
16-16: 화이트리스트 기반 정렬 허용 필드 적용은 적절합니다.직접 노출된 엔티티 필드에만 정렬을 제한해 정렬 인젝션/오류를 방지하고 있습니다. 현재 요구사항 기준으로 "id"만 허용하는 설계 타당합니다.
src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java (2)
18-22: 변경 사항이 적절합니다!Swagger 문서 인터페이스가 컨트롤러 구현체와 일치하도록
GroupInvitationListRequest를 사용하고PagingResponse를 반환하도록 올바르게 업데이트되었습니다.
25-28: 일관된 페이징 모델 적용이 잘 되었습니다!받은 초대 목록 조회 API도 동일한 페이징 패턴을 따르도록 적절히 수정되었습니다.
src/main/java/project/flipnote/group/service/GroupInvitationService.java (3)
63-63: 권한 검증 로직이 정책 서비스로 잘 분리되었습니다!
GroupInvitationPolicyService를 통한 권한 검증으로 단일 책임 원칙(SRP)을 잘 준수하고 있습니다.Also applies to: 86-86
67-67: 자기 자신 초대 방지 로직이 적절히 분리되었습니다!자기 초대 검증을 정책 서비스로 위임하여 비즈니스 로직이 깔끔해졌습니다.
151-152: 명확한 확인을 위해 아래 스크립트 실행 결과를 공유해 주세요:
GroupInvitationListRequest.getPageRequest()에서 실제로getSize() + 1처리가 어떻게 구현되어 있는지PagingRequest.getPageRequest()기본 동작PagingResponse.from(Page<T>)가 Spring Data JPA의Page객체를 어떻게 활용하는지 (특히hasNext,getTotalElements사용 여부)위 정보를 바탕으로, +1 처리 필요성 여부를 최종 검토하고 코멘트를 업데이트하겠습니다.
src/main/java/project/flipnote/group/service/GroupInvitationService.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (12)
src/main/java/project/flipnote/infra/email/EmailService.java (1)
9-9: sendGuestGroupInvitation 메서드 구현/호출부 정합성 검증 완료 및 인터페이스 계약 명세 제안ResendEmailService에서 새 메서드를 정상적으로 오버라이드했고, GuestGroupInvitationEventListener 호출부에서
clientProperties.getUrl()로 절대경로 URL을 전달하는 것을 확인했습니다.
인터페이스 수준에서 파라미터 불변식(Null/Blank, URL 형식)을 Javadoc으로 명시하여 안정성과 사용성을 향상시킬 것을 권장드립니다.
- src/main/java/project/flipnote/infra/email/ResendEmailService.java:72 –
sendGuestGroupInvitation오버라이드 구현 확인- src/main/java/project/flipnote/group/listener/GuestGroupInvitationEventListener.java:32 – 호출부에서 절대경로 URL 생성 및 전달 확인
- src/main/java/project/flipnote/infra/email/EmailService.java:9 – Javadoc 계약 명세 추가 제안
Javadoc 초안:
public interface EmailService { void sendEmailVerificationCode(String to, String code, int ttl); void sendPasswordResetLink(String to, String link, int ttl); + /** + * 비회원 그룹 초대 메일 발송. + * + * @param to 수신자 이메일 (검증된 형식, 비어있지 않음) + * @param groupName 표시용 그룹명 (빈 문자열 금지) + * @param registerUrl 회원가입 URL (절대경로, 신뢰 도메인) + */ void sendGuestGroupInvitation(String to, String groupName, String registerUrl); }src/main/java/project/flipnote/common/model/event/UserRegisteredEvent.java (1)
3-6: 단일 인자 생성자 호출 및 이메일 직로그 노출 여부 없음 확인검증 스크립트에서
new UserRegisteredEvent(...)호출 시 단일 인자 형태 사용log.info()/warn()/error()등에서event.email()직접 노출
모두 탐지되지 않아(출력 없음) 잔존 이슈는 없는 것으로 확인되었습니다.
이벤트 필드 순서/네이밍 일관성 고려(선택)
• 다른 이벤트들과 동일하게 식별자 우선(id → 부가정보) 방식으로userId, email순서로 재정렬하거나
• 현 상태(email, userId)를 유지할 경우 팀 컨벤션에 명시하여 통일 권장이메일(PII) 로그 노출 시 마스킹 처리 권장
src/main/java/project/flipnote/group/model/event/GuestGroupInvitationCreateEvent.java (1)
3-7: 이벤트 레코드에 입력 제약 추가 및 명명 규칙 일관성 검토
- 입력 유효성 제약 명시
- 이벤트 페이로드에도 검증 어노테이션(@Email, @notblank 등)을 추가하면, 검증 파이프라인과 결합 시 안전성을 높일 수 있습니다.
예시:public record GuestGroupInvitationCreateEvent( - String email, - String groupName + @Email @NotBlank String email, + @NotBlank String groupName ) { }- 이벤트 클래스명 일관성 검토
- 현재 프로젝트 내
GuestGroupInvitationCreateEvent를 제외하고는…CreatedEvent형식을 사용하는 레코드 이벤트가 확인되지 않습니다.- 앞으로 생성(created) 이벤트를 표현할 때
CreateEventvs.CreatedEvent중 하나를 선택하여 일관되게 사용하는 것을 권장드립니다(선택 사항).- 리네이밍 시 참조 위치
- 파일명 및 클래스명 변경:
src/main/java/project/flipnote/group/model/event/GuestGroupInvitationCreateEvent.java→GuestGroupInvitationCreatedEvent.java- 참조 코드 교체:
src/main/java/project/flipnote/group/listener/GuestGroupInvitationEventListener.java
- import 구문, 메소드명(
handleGuestGroupInvitationCreateEvent→handleGuestGroupInvitationCreatedEvent), recover 시그니처src/main/java/project/flipnote/group/service/GroupInvitationService.java
- import 구문,
eventPublisher.publishEvent(new GuestGroupInvitationCreateEvent…)호출부- 추가 권장 사항
- 관련 리스너/서비스의 오류 로그에 이메일을 그대로 노출하지 않도록 마스킹 유틸 적용 검토
src/main/resources/templates/email/guest-group-invitation.html (3)
24-28: groupName null 시 'null' 노출 가능성 — 엘비스 연산자로 안전 출력Thymeleaf의 th:text는 null이면 “null”로 렌더될 수 있습니다. 안전한 기본값을 표현식에 포함해 주세요.
- <b th:text="${groupName}">[그룹명]</b> 그룹에서 회원님을 초대하였습니다.<br/> + <b th:text="${groupName} ?: '[그룹명]'">[그룹명]</b> 그룹에서 회원님을 초대하였습니다.<br/>
30-35: 외부 링크 보안 + URL 인코딩 개선
- target="_blank" 사용 시 rel="noopener noreferrer" 추가 권장.
- URL은 링크 표현식(@{})로 감싸면 파라미터 인코딩 안전성이 높아집니다(토큰 포함 시 권장).
- <a th:href="${registerUrl}" target="_blank" - style="display:inline-block; padding:12px 24px; + <a th:href="@{${registerUrl}}" target="_blank" rel="noopener noreferrer" + style="display:inline-block; padding:12px 24px; background-color:#2D8CF0; color:#fff; font-weight:700; font-size:16px; border-radius:8px; text-decoration:none;">
10-12: 프리헤더 호환성 미세 개선(Outlook 등)일부 클라이언트에서 완전 숨김이 되지 않을 수 있어 mso-hide와 !important를 추가하면 안전합니다.
-<div style="display:none; font-size:1px; color:#333; line-height:1px; max-height:0; max-width:0; opacity:0; overflow:hidden;"> +<div style="display:none !important; mso-hide:all; font-size:1px; color:#333; line-height:1px; max-height:0; max-width:0; opacity:0; overflow:hidden;">src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java (1)
35-35: 로그의 PII(이메일) 마스킹 고려운영 로그에 이메일이 평문으로 남습니다. 보안/개인정보 관점에서 마스킹(예:
a***@example.com) 또는 해시(log join용)를 권장합니다. 팀 로깅 정책에 맞춰 일괄 정비를 검토해 주세요.src/main/java/project/flipnote/infra/email/ResendEmailService.java (1)
71-92: 게스트 그룹 초대 메일 UX/일관성 개선 제안구현 전반은 명확합니다. 다음 두 가지 개선을 제안합니다.
- 제목에 그룹명을 포함해 개봉률을 높입니다.
- 기존 메서드들(
sendEmailVerificationCode,sendPasswordResetLink)과 로그 키를to={}로 맞춰 일관성을 유지합니다.- 가능하다면 텍스트 본문(
text)도 포함해 일부 클라이언트에서의 가독성을 보완합니다.적용 예시:
- .subject("그룹 초대 안내") + .subject(String.format("그룹 초대 안내: %s", groupName)) .html(html) + .text(String.format("'%s' 그룹에서 초대했습니다. 가입/로그인은 다음 링크에서 진행해 주세요: %s", groupName, registerUrl)) .build(); ... - log.error("비회원 그룹 초대 발송 실패: email={}", to, e); + log.error("비회원 그룹 초대 발송 실패: to={}", to, e);src/main/java/project/flipnote/group/listener/GuestGroupInvitationEventListener.java (1)
36-38: 게스트 초대 전송 실패 로그의 PII 최소화현재 이메일이 평문으로 로그에 남습니다. 운영 정책에 따라 이메일 마스킹 또는 해시 사용을 권장합니다. 예:
log.error("... emailMasked={}, groupName={}", mask(event.email()), event.groupName(), ex);src/main/java/project/flipnote/group/service/GroupInvitationService.java (3)
64-69: 권한/셀프 초대 검증의 정책 서비스 위임 Good
groupInvitationPolicyService로 위임되어 책임이 명확히 분리되었습니다. 다만, 동일 클래스 내 다른 구간(아래getOutgoingInvitations)에서는groupService.hasPermission을 직접 호출하고 있어 일관성이 다소 흔들립니다. 정책 일원화를 검토해 주세요.
147-149: 권한 체크 경로 일관화 제안여기서는
groupService.hasPermission을 직접 호출합니다만, 상단 생성/삭제 경로는groupInvitationPolicyService를 사용합니다. 정책 변경의 단일 진입점을 유지하려면 본 구간도 정책 서비스로 일원화하는 편이 유지보수에 유리합니다.
256-259: 그룹명 조회 실패 시 예외 발생 흐름 적절존재하지 않는 그룹에 대한 방어가 명확합니다. 이벤트 발행 전 조회 비용을 줄이려면, 필요 시
createGuestInvitation상단의groupRepository.getReferenceById(groupId)로부터 이름을 함께 가져오는 방식을 고려할 수 있으나, 현재 수준에서는 과도한 최적화는 아닙니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (11)
src/main/java/project/flipnote/auth/service/AuthService.java(1 hunks)src/main/java/project/flipnote/common/model/event/UserRegisteredEvent.java(1 hunks)src/main/java/project/flipnote/group/listener/GuestGroupInvitationEventListener.java(1 hunks)src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java(1 hunks)src/main/java/project/flipnote/group/model/event/GuestGroupInvitationCreateEvent.java(1 hunks)src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java(1 hunks)src/main/java/project/flipnote/group/service/GroupInvitationService.java(9 hunks)src/main/java/project/flipnote/infra/email/EmailService.java(1 hunks)src/main/java/project/flipnote/infra/email/ResendEmailService.java(1 hunks)src/main/resources/application.yml(1 hunks)src/main/resources/templates/email/guest-group-invitation.html(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- src/main/resources/application.yml
🧰 Additional context used
🧬 Code graph analysis (10)
src/main/java/project/flipnote/infra/email/ResendEmailService.java (2)
src/main/java/project/flipnote/notification/listener/GroupInvitationCreateEventListener.java (1)
Slf4j(16-37)src/main/java/project/flipnote/notification/service/NotificationService.java (1)
Transactional(95-110)
src/main/java/project/flipnote/infra/email/EmailService.java (3)
src/main/java/project/flipnote/notification/listener/GroupInvitationCreateEventListener.java (1)
Slf4j(16-37)src/main/java/project/flipnote/notification/service/NotificationService.java (1)
Transactional(95-110)src/main/java/project/flipnote/group/model/GroupInvitationCreateRequest.java (1)
GroupInvitationCreateRequest(6-10)
src/main/java/project/flipnote/common/model/event/UserRegisteredEvent.java (6)
src/main/java/project/flipnote/common/model/event/UserWithdrawnEvent.java (1)
UserWithdrawnEvent(3-6)src/main/java/project/flipnote/common/model/event/GroupInvitationCreatedEvent.java (1)
GroupInvitationCreatedEvent(3-7)src/main/java/project/flipnote/auth/model/UserRegisterResponse.java (1)
UserRegisterResponse(3-10)src/main/java/project/flipnote/common/model/event/GroupJoinRequestedEvent.java (1)
GroupJoinRequestedEvent(5-10)src/main/java/project/flipnote/common/model/request/UserCreateCommand.java (1)
UserCreateCommand(3-12)src/main/java/project/flipnote/auth/model/UserRegisterRequest.java (1)
UserRegisterRequest(11-40)
src/main/java/project/flipnote/group/model/event/GuestGroupInvitationCreateEvent.java (2)
src/main/java/project/flipnote/common/model/event/GroupInvitationCreatedEvent.java (1)
GroupInvitationCreatedEvent(3-7)src/main/java/project/flipnote/group/model/GroupInvitationCreateRequest.java (1)
GroupInvitationCreateRequest(6-10)
src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java (1)
src/main/java/project/flipnote/notification/listener/GroupInvitationCreateEventListener.java (3)
Slf4j(16-37)Recover(33-36)Async(23-31)
src/main/java/project/flipnote/auth/service/AuthService.java (4)
src/main/java/project/flipnote/common/model/event/UserWithdrawnEvent.java (1)
UserWithdrawnEvent(3-6)src/main/java/project/flipnote/auth/model/UserRegisterResponse.java (1)
UserRegisterResponse(3-10)src/main/java/project/flipnote/auth/entity/UserAuth.java (1)
Getter(18-76)src/main/java/project/flipnote/auth/listener/UserWithdrawnEventListener.java (1)
Slf4j(17-42)
src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java (1)
src/main/java/project/flipnote/group/entity/GroupInvitation.java (1)
Getter(26-104)
src/main/resources/templates/email/guest-group-invitation.html (4)
src/main/java/project/flipnote/group/model/GroupInvitationCreateRequest.java (1)
GroupInvitationCreateRequest(6-10)src/main/java/project/flipnote/group/entity/GroupInvitation.java (1)
Getter(26-104)src/main/java/project/flipnote/notification/listener/GroupInvitationCreateEventListener.java (1)
Slf4j(16-37)src/main/java/project/flipnote/notification/service/NotificationService.java (1)
Transactional(95-110)
src/main/java/project/flipnote/group/listener/GuestGroupInvitationEventListener.java (2)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (1)
RequiredArgsConstructor(35-262)src/main/java/project/flipnote/infra/email/ResendEmailService.java (1)
Slf4j(16-93)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (3)
src/main/java/project/flipnote/group/model/GroupInvitationListRequest.java (1)
GroupInvitationListRequest(8-14)src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java (1)
RequiredArgsConstructor(19-50)src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java (2)
Tag(15-32)Operation(26-31)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (3)
src/main/java/project/flipnote/common/model/event/UserRegisteredEvent.java (1)
4-5: 이벤트 페이로드 확장(LGTM)회원가입 후속 처리에서 userId를 함께 전달하는 설계는 합리적입니다. 리스너 측 참조 변경이 완료됐다면 그대로 진행해도 좋습니다.
src/main/java/project/flipnote/auth/service/AuthService.java (1)
81-81: 이벤트 발행(userId 포함) 및 리스너 설정 검증 완료
publishEvent(new UserRegisteredEvent(email, userId))외에 구(舊) 시그니처 호출(한 개 인자만 전달) 없음 확인됨- 일반
@EventListener로UserRegisteredEvent를 처리하는 핸들러도 모두 제거된 상태 확인됨
트랜잭션 내부 발행 + AFTER_COMMIT 리스너 설정으로 데이터 정합성 보장되므로 승인합니다.src/main/java/project/flipnote/group/service/GroupInvitationService.java (1)
142-169: 컨트롤러·Docs 페이징 시그니처 변경 반영 확인 완료컨트롤러(
GroupInvitationQueryController)의getOutgoingInvitations,getIncomingInvitations메서드 및 OpenAPI Docs 인터페이스(GroupInvitationQueryControllerDocs) 모두 기존page/size파라미터 대신GroupInvitationListRequest를 사용하도록 정상 반영된 것을 확인했습니다. 추가 조치가 필요하지 않습니다.
| groupInvitationService.convertGuestInvitationToMember(event.email(), event.userId()); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
게스트 초대 → 회원 변환 시 업데이트 범위 한정 및 중복 안전성 점검 필요
현재 convertGuestInvitationToMember(event.email(), event.userId())는 이메일 기준으로 초대 레코드의 inviteeUserId를 일괄 갱신합니다. 다음을 확인/보완해 주세요.
- 만료·수락·거절된 초대까지 모두 갱신되는지 여부: 운영/통계 일관성 측면에서
PENDING상태만 갱신하는 것이 안전합니다. - 중복 안전성: 테이블에
uq_group_invitee_user/uq_group_invitee_email제약이 있으므로, 동일 그룹에 이미 회원 초대가 존재하는 케이스와의 충돌 가능성을 사전에 차단(미존재 시에만 갱신 or 중복 시 게스트 초대 삭제 등)하는 방어 로직이 필요합니다. - 대량 갱신 결과(영향 행 수)를 로그로 남겨 관측 가능성을 높이는 것도 추천합니다.
레포지토리 레벨에서 WHERE 절에 상태/널 조건을 추가하는 최소 수정안을 별도 코멘트에 첨부했습니다. 서비스 레벨에서의 중복 처리까지 결합하면 더 안전합니다.
다음으로 convertGuestInvitationToMember 메서드 구현부를 확인하여 업데이트 쿼리의 WHERE 절과 중복 처리 로직 여부를 검증합니다.
🏁 Script executed:
#!/bin/bash
# Java 파일 내 convertGuestInvitationToMember 메서드 정의 및 호출부 검색
rg -nP --type=java 'convertGuestInvitationToMember' -C3Length of output: 1622
추가로 bulkUpdateInviteeUserId 구현과 GroupInvitation 엔티티의 제약조건을 확인합니다.
🏁 Script executed:
#!/bin/bash
# repository 메서드 시그니처 및 쿼리 확인
rg -nP --type=java 'bulkUpdateInviteeUserId' -C10
# GroupInvitationRepository 인터페이스 위치 확인
rg -nP --type=java 'interface GroupInvitationRepository' -C5
# 엔티티 클래스 제약조건 확인
rg -nP --type=java 'class GroupInvitation' -C10
# @Table 어노테이션을 통한 유니크 제약정보 확인
rg -nP --type=java '@Table' -C5 src/main/javaLength of output: 37638
convertGuestInvitationToMember 호출부뿐 아니라, 실제 업데이트 쿼리와 엔티티의 상태 필드를 함께 확인해 보겠습니다.
🏁 Script executed:
#!/bin/bash
# 1. 서비스 레이어 메서드 확인 (convertGuestInvitationToMember 호출 직전·직후 로직 포함)
sed -n '180,220p' src/main/java/project/flipnote/group/service/GroupInvitationService.java
# 2. bulkUpdateInviteeUserId 쿼리와 주석 확인
sed -n '30,70p' src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java
# 3. GroupInvitation 엔티티에서 status, expiredAt 필드 정의 확인
sed -n '1,200p' src/main/java/project/flipnote/group/entity/GroupInvitation.javaLength of output: 5342
게스트 초대 → 회원 변환 로직 보완 필요
현재 convertGuestInvitationToMember(email, userId)는 이메일만 조건으로 모든 상태의 초대를 일괄 갱신해 PENDING이 아닌 초대까지 업데이트되며, 제약 위반 위험과 로깅 누락이 있습니다. 아래 보완을 반드시 적용해 주세요.
• GroupInvitationRepository.java (라인 43–47)
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
- UPDATE GroupInvitation gi
- SET gi.inviteeUserId = :inviteeUserId
- WHERE gi.inviteeEmail = :inviteeEmail
+ UPDATE GroupInvitation gi
+ SET gi.inviteeUserId = :inviteeUserId
+ WHERE gi.inviteeEmail = :inviteeEmail
+ AND gi.status = project.flipnote.group.entity.GroupInvitationStatus.PENDING
+ AND gi.inviteeUserId IS NULL
""")
int bulkUpdateInviteeUserId(@Param("inviteeEmail") String inviteeEmail,
@Param("inviteeUserId") Long inviteeUserId);• GroupInvitationService.java (라인 197–200)
@Transactional
public void convertGuestInvitationToMember(String email, Long userId) {
- groupInvitationRepository.bulkUpdateInviteeUserId(email, userId);
+ int updated = groupInvitationRepository.bulkUpdateInviteeUserId(email, userId);
+ log.info("Converted {} guest invitations to members (email={}, userId={})",
+ updated, email, userId);
}• 중복 방어 로직 추가 (GroupInvitationService.java)
- 동일 그룹에 이미
inviteeUserId기반 초대가 있는 경우 업데이트 대상에서 제외하거나 별도 처리 - 예:
List<Long> pendingGroupIds = groupInvitationRepository.findPendingGroupIdsByEmail(email); List<Long> existingGroupIds = groupInvitationRepository.findGroupIdsByInviteeUserId(userId); List<Long> safeGroupIds = pendingGroupIds.stream() .filter(gid -> !existingGroupIds.contains(gid)) .toList(); // safeGroupIds만 업데이트하도록 repository 메서드 추가 호출
위 세 가지를 반영하면
PENDING상태 초대만 갱신- 제약 위반 없이 중복 방어
- 영향 행 수 로깅으로 관측성 확보
반드시 수정 후 재검토 부탁드립니다.
🤖 Prompt for AI Agents
In
src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java
around lines 30–31, the call to
groupInvitationService.convertGuestInvitationToMember(event.email(),
event.userId()) must be backed by repository and service changes so only PENDING
invitations are updated, duplicates are avoided, and affected-row logging is
added: update GroupInvitationRepository (lines ~43–47) to expose a method that
returns pending group IDs by email and a method that updates inviteeUserId by a
list of groupIds (so the update affects only those IDs); update
GroupInvitationService (lines ~197–200) to first fetch pendingGroupIds =
findPendingGroupIdsByEmail(email), fetch existingGroupIds =
findGroupIdsByInviteeUserId(userId), compute safeGroupIds = pendingGroupIds
minus existingGroupIds, call the repository update only with safeGroupIds, log
the number of rows affected and any skipped group IDs, and ensure the service
defensively skips or records groups where a duplicate invitee already exists to
avoid constraint violations.
| @Modifying(clearAutomatically = true, flushAutomatically = true) | ||
| @Query(""" | ||
| UPDATE GroupInvitation gi | ||
| SET gi.inviteeUserId = :inviteeUserId | ||
| WHERE gi.inviteeEmail = :inviteeEmail | ||
| """) | ||
| int bulkUpdateInviteeUserId(@Param("inviteeEmail") String inviteeEmail, @Param("inviteeUserId") Long inviteeUserId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
bulkUpdateInviteeUserId의 갱신 범위가 과도함(PENDING 한정 및 NULL 체크 필요)
현재 쿼리는 상태/널 조건 없이 이메일 일치 항목 전체의 inviteeUserId를 갱신합니다. 이는 과거(만료·수락·거절) 레코드까지 이력 오염을 야기할 수 있으며, 특정 상황에서 고유 제약과도 충돌할 여지가 있습니다. 최소한 PENDING 상태이면서 inviteeUserId IS NULL 인 레코드만 갱신하도록 범위를 좁혀 주세요.
- @Query("""
- UPDATE GroupInvitation gi
- SET gi.inviteeUserId = :inviteeUserId
- WHERE gi.inviteeEmail = :inviteeEmail
- """)
+ @Query("""
+ UPDATE GroupInvitation gi
+ SET gi.inviteeUserId = :inviteeUserId
+ WHERE gi.inviteeEmail = :inviteeEmail
+ AND gi.status = project.flipnote.group.entity.GroupInvitationStatus.PENDING
+ AND gi.inviteeUserId IS NULL
+ """)
int bulkUpdateInviteeUserId(@Param("inviteeEmail") String inviteeEmail, @Param("inviteeUserId") Long inviteeUserId);추가로, 가장 안전한 방식은 서비스 레벨에서 이메일 기준 PENDING 게스트 초대를 조회한 뒤, 동일 그룹에 동일 사용자 대상 초대가 이미 있는 경우 해당 게스트 초대를 제거하고, 그렇지 않으면 초대 엔티티에 inviteeUserId를 할당하는 방식의 “머지” 처리입니다. 필요 시 해당 접근에 맞춘 저장소/엔티티 보조 메서드도 제안드릴 수 있습니다.
GroupInvitation 엔티티에 status 및 inviteeUserId 필드가 정확히 존재하는지 확인하기 위해 아래 스크립트를 실행해 주세요.
🏁 Script executed:
#!/bin/bash
# 1. 엔티티 파일 경로 조회
entity_file=$(fd --full-path "GroupInvitation.java" src/main/java/project/flipnote/group/entity)
echo "Entity path: $entity_file"
# 2. 클래스 선언부 및 주요 필드 확인 (상위 200줄)
sed -n '1,200p' "$entity_file"
# 3. inviteeUserId, status 필드 위치 확인
grep -n -E "inviteeUserId|status" "$entity_file"Length of output: 4391
필수: bulkUpdateInviteeUserId 쿼리에 PENDING 상태 및 inviteeUserId NULL 조건 추가
GroupInvitation 엔티티에 inviteeUserId(nullable)와 status(Enum, NOT NULL) 필드가 존재함을 확인했습니다.
현 쿼리는 이메일 일치 항목 전체를 업데이트하여, 만료·수락·거절된 레코드까지 오염시킬 수 있고, 고유 제약 위반 위험이 있습니다.
수정 대상:
- 파일: src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java
- 메서드: bulkUpdateInviteeUserId (41–47행)
제안 드리는 변경사항:
- @Query("""
- UPDATE GroupInvitation gi
- SET gi.inviteeUserId = :inviteeUserId
- WHERE gi.inviteeEmail = :inviteeEmail
- """)
+ @Query("""
+ UPDATE GroupInvitation gi
+ SET gi.inviteeUserId = :inviteeUserId
+ WHERE gi.inviteeEmail = :inviteeEmail
+ AND gi.status = project.flipnote.group.entity.GroupInvitationStatus.PENDING
+ AND gi.inviteeUserId IS NULL
+ """)
int bulkUpdateInviteeUserId(@Param("inviteeEmail") String inviteeEmail, @Param("inviteeUserId") Long inviteeUserId);추가 제안 (선택):
- 서비스 레벨에서 PENDING 초대만 조회·병합(merge) 처리하는 방식으로, 동일 그룹에 중복 초대가 있는 경우 기존 초대를 삭제하거나 업데이트하도록 구현할 수 있습니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @Modifying(clearAutomatically = true, flushAutomatically = true) | |
| @Query(""" | |
| UPDATE GroupInvitation gi | |
| SET gi.inviteeUserId = :inviteeUserId | |
| WHERE gi.inviteeEmail = :inviteeEmail | |
| """) | |
| int bulkUpdateInviteeUserId(@Param("inviteeEmail") String inviteeEmail, @Param("inviteeUserId") Long inviteeUserId); | |
| @Modifying(clearAutomatically = true, flushAutomatically = true) | |
| @Query(""" | |
| UPDATE GroupInvitation gi | |
| SET gi.inviteeUserId = :inviteeUserId | |
| WHERE gi.inviteeEmail = :inviteeEmail | |
| AND gi.status = project.flipnote.group.entity.GroupInvitationStatus.PENDING | |
| AND gi.inviteeUserId IS NULL | |
| """) | |
| int bulkUpdateInviteeUserId(@Param("inviteeEmail") String inviteeEmail, | |
| @Param("inviteeUserId") Long inviteeUserId); |
🤖 Prompt for AI Agents
In
src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java
around lines 41–47, the bulkUpdateInviteeUserId JPQL currently updates all rows
matching inviteeEmail; change the WHERE clause to only affect pending,
unassigned invitations by adding "AND gi.status = <PENDING_ENUM_LITERAL> AND
gi.inviteeUserId IS NULL" (replace <PENDING_ENUM_LITERAL> with the
fully-qualified enum literal for your Invitation status, e.g.
com.my.pkg.InvitationStatus.PENDING, or bind a :status param if you prefer);
keep the method signature/params consistent and ensure the enum reference
compiles.
| public void convertGuestInvitationToMember(String inviteeEmail, Long inviteeUserId) { | ||
| groupInvitationRepository.bulkUpdateInviteeUserId(inviteeEmail, inviteeUserId); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
게스트 초대 → 회원 변환 로직의 중복 안전성/이력 보존 개선
현재는 이메일 기준 일괄 업데이트로 처리합니다. 다음 리스크를 해소해 주세요.
- 상태 무관 갱신으로 과거 이력 오염 가능성(PENDING 한정 필요) — 저장소 쿼리 보완안은 레포지토리 코멘트에 제시했습니다.
- 같은 그룹/동일 사용자 대상 초대가 이미 존재하는 경우 고유 제약 충돌 가능성 — 서비스 레벨에서 “중복 시 게스트 초대 제거, 아니면 지정” 형태의 머지 로직이 안전합니다.
머지 접근을 위한 최소 변경안(서비스/저장소/엔티티 보조 메서드 추가)을 제안드립니다.
- public void convertGuestInvitationToMember(String inviteeEmail, Long inviteeUserId) {
- groupInvitationRepository.bulkUpdateInviteeUserId(inviteeEmail, inviteeUserId);
- }
+ public void convertGuestInvitationToMember(String inviteeEmail, Long inviteeUserId) {
+ // 1) 이메일 기반 PENDING 게스트 초대 조회
+ var guestInvitations = groupInvitationRepository
+ .findAllByInviteeEmailAndStatus(inviteeEmail, GroupInvitationStatus.PENDING);
+
+ for (var gi : guestInvitations) {
+ Long groupId = gi.getGroup().getId();
+ // 2) 동일 그룹에 이미 해당 사용자 대상 PENDING 초대가 있으면 게스트 초대 정리
+ if (groupInvitationRepository
+ .existsByGroup_IdAndInviteeUserIdAndStatus(groupId, inviteeUserId, GroupInvitationStatus.PENDING)) {
+ groupInvitationRepository.delete(gi);
+ continue;
+ }
+ // 3) 아니면 게스트 초대를 회원 초대로 전환
+ gi.assignInviteeUser(inviteeUserId);
+ }
+ }추가 필요 메서드들(파일 외부 변경, 참고용):
- 저장소:
List<GroupInvitation> findAllByInviteeEmailAndStatus(String inviteeEmail, GroupInvitationStatus status);- 엔티티(
GroupInvitation):
public void assignInviteeUser(Long inviteeUserId) {
this.inviteeUserId = inviteeUserId;
}위 머지 로직과 함께 저장소의 bulk 업데이트 WHERE 절 보강을 병행하면, 대량 케이스 최적화와 정확성을 동시에 확보할 수 있습니다.
🤖 Prompt for AI Agents
In src/main/java/project/flipnote/group/service/GroupInvitationService.java
around lines 198-200, replace the blind bulkUpdate by loading pending
invitations for the email, merging duplicates and preserving history: call
repository.findAllByInviteeEmailAndStatus(inviteeEmail, PENDING), iterate
results and for each invitation call the new entity helper
assignInviteeUser(inviteeUserId) to set inviteeUserId; if multiple invitations
exist for the same group prefer keeping one (e.g. if an existing invitation
already has inviteeUserId non-null, delete the guest-only duplicates via
repository.delete), otherwise assign the user id to a single record and persist
changes; also keep the repository bulk-update WHERE clause restricted to status
= PENDING as suggested so large-case optimization remains intact.
📝 변경 내용
✅ 체크리스트
💬 기타 참고 사항
Summary by CodeRabbit